'''
@since: Jan 2, 2012
@author: Chris Giacofei
@version: 2012.01.30


This module is intended to be used with the Hide My Ass vpn service. Some 
HMA servers seem to have a tendency to disconnect without notice (leaving the user 
exposed).  This script solves the issue by re-launching openvpn if there is a disconnect.

Currently the script keeps openvpn running and logs server names and connection uptime
to an xml for file later analysis.  Future additions will hopefully include preferential 
server selection based on past performance.  The ability to check the connection speed
of a server while it is running would also be desirable.

@change: 2012.01.30:
    1. Connects to a new random server every 48 hours
    2. Additional error handling
    3. Reason for vpn restart is written the xml log
        
@change: 2012.01.14:
    1. Much refactoring to simplify program flow.
    2. Cleaned up program output and logging
    3. Bug Fix: Changed server selection so the same server is not called repeatedly if
       if there is a connection problem.
    4. New: XML log now includes reason for VPN disconnection.

@change: 2012.01.12:
    1. Bug Fix: Fixed scrit crashed if unable to check IP at whatismyip.org
       IP check-in now returns a string instead of -1 if unable to reach URL
    2. Bug Fix: Fixed downloading wrong client.cfg file from HMA server
    
@change: 2012.01.07:
    1. Bug Fix: Fixed str concatenate error where int type would cause exception
    2. Bug Fix: Fixed XML logging error
    
@change: 2012.01.06a:
    1. New: Added xml logging
    
@change: 2012.01.06:
    1. New: Add exception handler to rand_server(), now returns -1
       if server retrieval fails.
    2. New: Connection loop restarts if server choice is -1
    
@change: 2012.01.04:
    1. New: Added status output to text file every 5 minutes

@todo: Calculate connection speeds if possible
@todo: Choose server based on best performance

'''

import os
import sys
import time
import subprocess
import urllib2
import random
import re

import logxml

# URL's
LIST = 'http://vpn.hidemyass.com/vpnconfig/countries.php'
CLCONF = 'http://vpn.hidemyass.com/vpnconfig/client_config.php?win=1&loc='
check_ip_url = 'http://whatismyip.org'

# External files
script_file = 'hma_start.py' # This file
txt_log = 'vpnlog.txt' # Detailed text output
xml_log = 'vpnlog.xml' # Connection statistics logging
txt_status = 'status.txt' # Status log updated every 5 minutes
hmaauth = '/root/hmaauth.conf' # HMA login credentials

# Useful constants
DAY = 86400

# Start vpn command sequence
openvpn_cmd = ['openvpn', '--config', 'client.cfg', '--auth-user-pass', hmaauth]

def check_in():
    '''Returns the external IP address of the computer using http://whatismyip.org
    
    @return: IP address as string or 'IP-Unkown' if IP is not determined within 20 seconds.
    '''
    get_ip = False
    ip_start = time.time()
    
    while get_ip == False:
        try:
            ip = urllib2.urlopen(check_ip_url).read()
            get_ip = True
        except:
            if time.time() - ip_start > 20:
                ip = 'IP-Unkown'
                get_ip = True
            print('retry check ip')
            time.sleep(3)
    return ip
    
def write_log(output, aw='a', log_file=txt_log):
    '''Appends the given output to a log file
    
    @param output: Text to be written to the file
    @type output: string
    
    @param aw: file open parameter (a=append, w=overwrite)
    @type aw: strign
    
    @param log_file: File to write to
    @type log_file: string
    '''
    log_out = open(log_file, aw)
    log_out.write(output + '\n')
    log_out.close()

def rand_server(slist):
    '''Accepts a URL to a list of servers and returns a random 
    item from the list.
    
    @param slist: The list of server_list to select from
    @type slist: string
    
    @return: A string with the name of the server chosen
    '''
    try:
        server_list = urllib2.urlopen(slist).read().split('\n')
        index = random.randrange(0,len(server_list))
        server = server_list[index]
    except:
        server = 'failed'
    
    return server

def get_time():
    '''Returns a nicely formatted current time
    '''
    return time.asctime( time.localtime(time.time()))

def write_config(filename, text):
    '''Writes given text to a file. Will over-write 
    existing text in the file.
    
    @param filename: Name of file to write to
    @type filename: string
    
    @param text: Text to be writtent to the file
    @type text: string
    '''
    f = open(filename, 'w')
    f.write(text)

def time_convert(seconds):
    '''Converts time in seconds into d:h:m:s
    '''
    m, s = divmod(seconds, 60)
    h, m = divmod(m, 60)
    d, h = divmod(h, 24)
    return "%d:%02d:%02d:%02d" % (d, h, m, s)

def restart_program():
    """Restarts the current program.
    Note: this function does not return. Any cleanup action (like
    saving data) must be done before calling this function."""
    
    python = sys.executable
    os.execl(python, python, * sys.argv)

def get_version():
    '''Returns the current of the script in the main directory
    '''
    
    py_file = open(script_file, 'r')
    for line in py_file.readlines():
        if re.search('@version', line) != None:
            return line.strip('\n').split(' ')[1]
    
def new_version(old_ver):
    '''Checks whether the current script in main directory is a newer version
    than when the script was first started.
    
    @param old_ver: Currently running version of the script
    @type old_ver: string
    
    @return: True if a new version is available to run, otherwise returns false.
    '''
    
    new_ver = get_version().strip('\n').split('.')
    old_ver = old_ver.split('.')
        
    if new_ver[0] > old_ver[0]:
        return True
    if new_ver[0] == old_ver[0] and new_ver[1] > old_ver[1]:
        return True
    if new_ver[0] == old_ver[0] and new_ver[1] == old_ver[1] and new_ver[2] > old_ver[2]:
        return True
    
    return False

def vpn_running(my_ip, cur_version, duration):
    '''Runs open vpn in a loop until an error or stop contition occurs
    
    @param my_ip: ISP IP address
    @param cur_version: Version number of currently running script file
    @param duration: Length of time (seconds) to run before switching to new server
    
    @type my_ip: string
    @type cur_version: string
    @type duration: integer
    
    @return: Returns the start and end times as well as vpn IP address
             and disconnect reason
    '''
    prog = subprocess.Popen(openvpn_cmd, stdout=subprocess.PIPE)

    start = time.time()
    while prog.poll() == None:
        print('Polling OpenVPN')
        time.sleep(30)

        
        vpn_ip = check_in()
        
        # restart if unable to verify IP
        if vpn_ip == 'IP-Unkown':
            print(vpn_ip)
            err = 'Unable to verify IP'
            end = time.time()
            prog.kill()
            break
        
        # Restart if IP address is not hidden
        elif vpn_ip == my_ip:
            print(vpn_ip,my_ip)
            err = 'VPN Not Connected'
            end = time.time()
            prog.kill()
            break
        
        # Restart if script has been updated to newer version
        elif new_version(cur_version):
            write_log(get_time() + ' >>> Updating to new version: ' + get_version())
            end = time.time()
            err = 'New Version'
            prog.kill()
            break
        
        # Stop service if time limit hit
        elif time.time() - start > duration:
            write_log(get_time() + ' >>> Time limit hit, selecting new server.')
            end = time.time()
            err = 'Server Switch'
            prog.kill()
            break
        else:
            err = 'OpenVPN Terminated'
      
        print('write to log')
        write_log(get_time() + '\n' +
                  'Connection Active\n' +
                  'IP address is: ' + vpn_ip, 
                  aw='w', 
                  log_file=txt_status)
                    
        time.sleep(270)
    
    print(err)
    return start, end, vpn_ip, err

def main():
    '''
    Main function, starts hma script as subprocess.
    Checks for proper VPN connection every 5 minutes and 
    restarts VPN if IP is same as unmasked IP.
    '''
    print('script started')
    xml_entry = {}
    stay_conn = True
    
    os.system('killall openvpn')
    
    #get the current version of program and record
    cur_version = get_version()
    xml_entry['version'] = cur_version
    write_log(get_time() + ' >>> Current Version: ' + cur_version)
    
    # Get current unprotected IP
    my_ip = check_in()
    xml_entry['isp_ip'] = my_ip
    write_log(get_time() + ' >>> ISP IP address: ' + my_ip)
    print(my_ip)

    # Select a random server
    SERVER = rand_server(LIST).rstrip()
    xml_entry['server'] = SERVER
    write_log(get_time() + ' >>> Connect to HMA Server: ' + SERVER)
    print(SERVER)

    # Get the client config file for the selected server
    SERVER = SERVER.replace(' ', '%20')
    write_config('client.cfg',urllib2.urlopen(CLCONF + SERVER).read())
    
    # Continue only if initial IP verification and Server selection were successful
    if my_ip != 'IP-Unkown' and SERVER != 'failed':
        write_log(get_time() + ' >>> Current IP Address: ' + my_ip)
                        
        # Runs openvpn and returns info if it stops running
        # Selects a new random server every 2 days
        start, end, vpn_ip, err = vpn_running(my_ip, cur_version, DAY*2)
        
        # Setup information for logging after openvpn stops running
        xml_entry['timestamp'] = get_time()            
        xml_entry['start_time'] = str(start)
        xml_entry['end_time'] = str(end)
        xml_entry['uptime'] = time_convert(end - start)
        xml_entry['vpn_ip'] = vpn_ip
        xml_entry['error'] = err
        
        # Write disconnect reason and uptime to the log text file
        write_log(get_time() + ' >>> Disconnect: ' + err)
        write_log(get_time() + ' >>> UPTIME: ' + time_convert(end-start) + ' d:h:m:s')
        
        # Writes connection statistics to XML file
        logxml.write_log(xml_entry, xml_log)
    
    # Output to log if could not select server           
    elif SERVER == 'failed':
        write_log(get_time() + ' >>> Server selection error.')
    
    # Output to log if could not verify starting IP address
    elif my_ip == 'IP-Unkown':
        write_log(get_time() + ' >>> Unable to verify ISP IP address')
        my_ip = check_in()
    
    # For now this always evaluates true so script runs continuously
    if stay_conn == True:
        restart_program()
        
if __name__ == "__main__":
    main()